Что такое V8 heap snapshot и как его анализировать для поиска утечек памяти?

JuniorNode.js · Backend·Обновлено 13 сентября 2026
Коротко
Heap snapshot — это снимок всех объектов в куче V8 со ссылками между ними; утечки ищут, сравнивая несколько снапшотов и отслеживая объекты, число которых стабильно растёт, а также анализируя их retainers — цепочку ссылок, из-за которой объект не собирается GC.

Что такое heap snapshot

Heap snapshot — это моментальный снимок всех объектов, находящихся в куче (heap) движка V8, вместе со ссылками между ними. Он показывает, сколько памяти занимает каждый объект, сколько экземпляров каждого типа существует и какие объекты удерживают друг друга (дерево retainers). Снапшот используется, чтобы найти объекты, которые не должны жить долго, но остаются в памяти из-за забытых ссылок — то есть найти утечку.

Как снять snapshot

  • Chrome DevTools — запустить процесс с node --inspect и открыть вкладку Memory в chrome://inspect.
  • Программно — через встроенный модуль v8.
  • Сторонние пакетыheapdump, heap-profiler, которые позволяют снимать снапшот по сигналу прямо в production.

Как анализировать

Сравнение снапшотов (Comparison)

Самый надёжный способ поймать утечку — снять минимум три снапшота: до нагрузки, после нагрузки и ещё раз после принудительной сборки мусора и повторного цикла нагрузки. В DevTools во вкладке Memory выбирается режим Comparison, который показывает разницу между снимками: сколько объектов появилось (# New) и сколько осталось (# Delta). Если количество объектов определённого типа стабильно растёт от снимка к снимку — это признак утечки.

Shallow size, Retained size и Retainers

  • Shallow size — сколько памяти занимает сам объект.
  • Retained size — сколько памяти освободится, если удалить объект (объект плюс всё, что удерживается только через него).
  • Retainers — дерево, показывающее, кто ссылается на объект. Проходя по этому дереву снизу вверх, находят корневую ссылку, из-за которой объект не собирается GC: забытый обработчик события, замыкание, запись в глобальном кеше.

Поиск по классам объектов

В представлении Summary объекты группируются по конструктору (Array, Closure, (string) и т.д.). Полезно искать собственные классы приложения (например, User, Request) с аномально большим числом инстансов между снимками.

Типичные причины утечек в Node.js

  • Забытые слушатели событий (EventEmitter), которые не снимаются через removeListener.
  • Замыкания, удерживающие ссылки на большие объекты — например, callback в таймере или очереди задач.
  • Растущие глобальные кеши или Map без ограничения размера и TTL.
  • Незакрытые соединения (сокеты, стримы), которые держат буферы в памяти дольше, чем нужно.

Дополнительные инструменты

Помимо ручного анализа в DevTools, для production используют clinic.js heapprofiler, утилиту 0x, а также автоматическое снятие снапшота при превышении порога через process.memoryUsage().rss.

Что хочет услышать интервьюер

Понимание, что heap snapshot — это снимок графа объектов кучи V8 с их размерами и ссылками

Знание, что утечку ищут не по одному снапшоту, а по сравнению нескольких (Comparison view)

Умение объяснить разницу между shallow size и retained size

Понимание концепции retainers и способность найти корневую ссылку, удерживающую объект

Знание типичных источников утечек в Node.js: EventEmitter, замыкания, кеши, соединения

Пример: Снятие heap snapshot программно

const v8 = require('v8');
const fs = require('fs');

// делаем снимок кучи и пишем его в файл .heapsnapshot
function writeHeapSnapshot(filename) {
  const snapshotStream = v8.getHeapSnapshot();
  const fileStream = fs.createWriteStream(filename);
  snapshotStream.pipe(fileStream);
}

writeHeapSnapshot('before.heapsnapshot');
// ... нагрузка на приложение ...
writeHeapSnapshot('after.heapsnapshot');
// далее оба файла открываются во вкладке Memory в Chrome DevTools

Пример: Типичный источник утечки: забытый слушатель

class Cache {
  constructor(emitter) {
    this.data = new Map();
    // подписка без отписки — emitter будет держать ссылку на this вечно
    emitter.on('update', (item) => this.data.set(item.id, item));
  }

  destroy(emitter, handler) {
    // правильно: снимаем слушатель при уничтожении объекта
    emitter.removeListener('update', handler);
  }
}

Типичные ошибки

Делают вывод об утечке по одному снапшоту, а не по сравнению нескольких во времени

Путают shallow size и retained size

Не знают про режим Comparison в Chrome DevTools и ищут утечку вручную по общему списку объектов

Забывают вызвать сборку мусора перед сравнением снапшотов, из-за чего в диффе остаётся мусор, который скоро будет собран

Не могут назвать конкретные причины утечек в Node.js (только общие слова про 'память течёт')

Лучшие курсы по теме

изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Node.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее